分布式面试【zookeeper】
欢迎关注“Java后端技术全栈”
ZooKeeper
1. ZooKeeper 是什么?
直译:从名字上直译就是动物管理员,动物指的是 Hadoop 一类的分布式软件,管理员三个字体现了 ZooKeeper 的特点:维护、协调、管理、监控。
简述:有些软件你想做成集群或者分布式,你可以用 ZooKeeper 帮你来辅助实现。
特点:
最终一致性:客户端看到的数据最终是一致的。
可靠性:服务器保存了消息,那么它就一直都存在。
实时性:ZooKeeper 不能保证两个客户端同时得到刚更新的数据。
独立性(等待无关):不同客户端直接互不影响。
原子性:更新要不成功要不失败,没有第三个状态。
注意:回答面试题,切忌只是简单一句话回答,可以将你对概念的理解,特点等多个方面描述一下,哪怕你自己认为不完全切中题意的也可以说说,面试官不喜欢会打断你的,你的目的是让面试官认为你是好沟通的。当然了,如果不会可别装作会,说太多不专业的想法。
2. 描述一下 ZAB 协议
ZAB 协议是 ZooKeeper 自己定义的协议,全名 ZooKeeper 原子广播协议。
ZAB 协议有两种模式:Leader 节点崩溃了如何恢复和消息如何广播到所有节点。
整个 ZooKeeper 集群没有 Leader 节点的时候,属于崩溃的情况。比如集群启动刚刚启动,这时节点们互相不认识。比如运作 Leader 节点宕机了,又或者网络问题,其他节点 Ping 不通 Leader 节点了。这时就需要 ZAB 中的节点崩溃协议,所有节点进入选举模式,选举出新的 Leader。整个选举过程就是通过广播来实现的。选举成功后,一切都需要以 Leader 的数据为准,那么就需要进行数据同步了。
3. 四种类型的数据节点 Znode
持久节点:和我们存储到数据库的情况一样,存上了就不会丢失。
临时节点:你通过 ZK 客户端远程连接到 ZK 服务端,创建了临时节点,等待你的连接超时了,对不起这个节点就删除了,这就是临时节点。
持久顺序节点:首先它是持久化的,然后如果你创建了同名的节点,它不会说节点也存在,而且在名字后加上后缀。就像 Windows 的创建文件夹一样,后缀是数字从小到大,所以也就有了顺序性。
临时顺序节点:临时节点和顺序节点上面都解释过了,没错,就是它俩的组合,客户端连接超时节点就会消失,同名的节点后缀是排序的。
4. TCP 不是可靠连接吗,为什么分布式要考虑网络信息丢失的问题?
TCP 协议只能保证 TCP 层的可靠,所以数据到了应用层就不受控制了,而且我们需要的是应用层的可靠性。
看下图一目了然:
看到上图,这是在一台机器内部,可能传输出问题的概率不高,还有第二个原因:
TCP 协议只能保证同一个 TCP 连接内的消息是有序的。但在分布式系统中,发送数据,可能多个 TCP 连接发送一起发生一段数据,这个不同 TCP 连接的数据顺序 TCP 协议不会保证的,应用层协议也不保证,只能我们代码实现时控制。
看图 2:
5. 介绍一下两阶段提交协议 2PC
第一阶段:
协调者问所有参与者你那里是否能够提交数据,不管能不能都告诉我结果;
参与者收到数据就开始执行,将 Undo 和 Redo 信息写入日志,执行但是没有真正提交,等下一步操作再做最终处理,现在的情况是可进可退。
每个参与者都把结果如实告诉协调者。
第二阶段:当协调者从所有参与者获得的相应消息都为同意时:
协调者向所有参与者发出开干的命令;
参与者完成最终的数据操作;
参与者告诉协调者节点一定都搞定了;
协调者得到所有参与者节点的好消息,这才算是完成事务。
如果执行失败呢,也是一样的回滚流程。
两阶段提交看似不错,但是有个阻塞的问题,这个问题 两阶段无法解决,需要三阶段来解决。
6. 介绍一下三阶段提交协议 3PC
三阶段提交针对两阶段提交有两个改动点:
引入超时。如果等待时间过长那就超过了,不会一直等下去,解决了如果出现阻塞的麻烦。
多了一个准备阶段。一些可能出现的地方放到准备阶段了,这也是名字的由来。
第一阶段
2PC 的准备阶段很像。协调者向参与者发送处理数据的请求,参与者如果如果做好了就给好消息,搞砸了就给坏消息。
第二阶段
协调者根据参与者的好消息们来判断是否可以继续事务的 预提交操作。
如果都是好消息,那么就会执行事务的预执行。
发送请求:向参与者发送预提交请求。
预提交:参与者接收到预提交请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。
反馈:如果参与者成功的执行了事务操作,继续返回好消息,然后等待最终指令。
万一有谁向协调者报告了坏消息,或者超时了,协调者都没有接到参与者的消息,那么就执行事务的中断。
发送请求:协调者通知所有参与者中断事务。
中断事务:参与者收到中断请求之后,中断事务。
第三阶段最终的事务提交:
执行提交
协调者发送请求:协调通知所有参与者真正的提交事务请求。
参与者事务提交:参与者接收到真正提交事务的请求之后,执行正式的事务提交。
参与者反馈:事务提交后,告诉协调者好消息。
协调者完成事务:协调者接收到所有参与者的好消息之后,完成事务。
中断事务
规定时间内,协调者收到的好消息数量不够,那么就会执行中断事务。
协调者发送中断请求:协调者向所有参与者发送中断事务请求。
参与者事务回滚:参与者接收到中断事务请求之后,利用其在阶段二记录的 Undo 信息来执行事务的回滚操作。
参与者反馈:事务回滚之后,向协调者发送 回滚完了。
协调者中断事务:协调者接收到参与者反馈的回滚完成的消息之后,执行事务的中断。
三阶段提交的问题:
网络分区可能会带来问题。但是很少会有人再提四阶段了,事实上大家一般用 TCC 的思想来解决分布式事务的问题。
7. ZooKeeper 宕机如何处理?
ZooKeeper 本身也是集群,推荐配置奇数个服务器。因为宕机就需要选举,选举需要半数 +1 票才能通过,为了避免打成平手。进来不用偶数个服务器。
如果是 Follower 宕机了,没关系不影响任何使用。用户无感知。如果 Leader 宕机,集群就得停止对外服务,开始选举,选举出一个 Leader 节点后,进行数据同步,保证所有节点数据和 Leader 统一,然后开始对外提供服务。
为啥投票需要半数 +1,如果半数就可以的话,网络的问题可能导致集群选举出来两个 Leader,各有一半的小弟支持,这样数据也就乱套了。
8. 描述一下 ZooKeeper 的 session 管理的思想?
分桶策略:
简单地说,就是不同的会话过期可能都有时间间隔,比如 15 秒过期、15.1 秒过期、15.8 秒过期,ZooKeeper 统一让这些 session 16 秒过期。这样非常方便管理,看下面的公式,过期时间总是 ExpirationInterval 的整数倍。
计算公式:
ExpirationTime = currentTime + sessionTimeoutExpirationTime = (ExpirationTime / ExpirationInrerval + 1) * ExpirationInterval ,
见图片:
默认配置的 session 超时时间是在 2tickTime~20tickTime。
9. ZooKeeper Watcher 机制
监听是设计模式的一种思想,监听的节点发生了变化,监听者就会知道。一定按钮事件,异步通知都采用了类似的思想。
ZK 保存的节点,如果发生了变化,怎么通知使用者呢?可以通过 Watcher 机制。
整个使用流程:
客户端要先注册监听,指明要监听的节点。服务端把这些信息保存,操作节点时,检查是否有监听信息,有的话,就通知客户端。
注意,ZK Watcher 有个特点就是一次性的,这样看上去使用很麻烦。但是节省了资源,否则监听太多,或者监听一次就不用了,但是 ZK 还一遍一遍地通知是不合适的。一次性就表示有需要就注册。
10. ZooKeeper Server 的角色
Leader:
整个集群就一个,修改数据的操作只有 Leader 能执行,执行完同步给整个集群。
Follower:
处理客户端的读请求,也就是不改变数据的都可以处理,改变数据的转发请求给 Leader 服务器。
如果 Leader 宕机了,参与选举,自己可以给自己投票。
Observer:
处理客户端的读请求,也就是不改变数据的都可以处理,改变数据的转发请求给 Leader 服务器。这点和 Follower 很像。
不能投票,没有选举权和被选举权。
11. ZooKeeper Server 的状态
服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
LOOKING:集群宕机了,无法对外提供服务了,这时需要选举,所有节点都是这个状态。
FOLLOWING:Follower 节点正常情况下就是这个状态。
LEADING:Leader 节点正常情况下就是这个状态。
OBSERVING:Observer 节点正常情况下是这个状态。
对比三种角色,你会发现多了一种状态 Looking,这是在选举时大家的状态,表明需要选举,无法正常工作。
12. ZooKeeper 负载均衡和 Nginx 负载均衡区别
ZooKeeper:
不存在单点问题,zab 机制保证单点故障可重新选举一个 Leader
只负责服务的注册与发现,不负责转发,减少一次数据交换(消费方与服务方直接通信)
需要自己实现相应的负载均衡算法
Nginx:
存在单点问题,单点负载高数据量大,需要通过 KeepAlived 辅助实现高可用
每次负载,都充当一次中间人转发角色,本身是个反向代理服务器
自带负载均衡算法
13. ZooKeeper 的序列化
序列化:
内存数据,保存到硬盘需要序列化。
内存数据,通过网络传输到其他节点,需要序列化。
ZK 使用的序列化协议是 Jute,Jute 提供了 Record 接口。接口提供了两个方法:
serialize 序列化方法
deserialize 反序列化方法
要系列化的方法,在这两个方法中存入到流对象中即可。
14. Zxid 是什么,有什么作用
Zxid,也就是事务 id,为了保证事务的顺序一致性,ZooKeeper 采用了递增的事务 Zxid 来标识事务。proposal 都会加上了 Zxid。Zxid 是一个 64 位的数字,它高 32 位是 Epoch 用来标识朝代变化,比如每次选举 Epoch 都会加改变。低 32 位用于递增计数。
Epoch:可以理解为当前集群所处的年代或者周期,每个 Leader 就像皇帝,都有自己的年号,所以每次改朝换代,Leader 变更之后,都会在前一个年代的基础上加 1。这样就算旧的 Leader 崩溃恢复之后,也没有人听它的了,因为 Follower 只听从当前年代的 Leader 的命令。
15. 讲解一下 ZooKeeper 的持久化机制
什么是持久化?
数据,存到磁盘或者文件当中。
机器重启后,数据不会丢失。内存 -> 磁盘的映射,和序列化有些像。
ZooKeeper 的持久化:
SnapShot 快照,记录内存中的全量数据
TxnLog 增量事务日志,记录每一条增删改记录(查不是事务日志,不会引起数据变化)
为什么持久化这么麻烦,一个不可用吗?
快照的缺点,文件太大,而且快照文件不会是最新的数据。增量事务日志的缺点,运行时间长了,日志太多了,加载太慢。二者结合最好。
快照模式:
将 ZooKeeper 内存中以 DataTree 数据结构存储的数据定期存储到磁盘中。
由于快照文件是定期对数据的全量备份,所以快照文件中数据通常不是最新的。
见图片:
16. 投票信息的五元组
Leader:被选举的 Leader 的 SID
Zxid:被选举的 Leader 的事务 ID
Sid:当前服务器的 SID
electionEpoch:当前投票的轮次
peerEpoch:当前服务器的 Epoch
Epoch > Zxid > Sid
Epoch,Zxid 都可能一致,但是 Sid 一定不一样,这样两张选票一定会 PK 出结果。
17. Quorum 与脑裂
当某选票,占参与选举的数量的一半以上,选举结果
脑裂:大脑裂开了,成了两个大脑。这时数据不一致,客户端访问的结果就很佛系了。
半数以上的选票就是解决之道,只有拿到大于半数的选票才能成为大脑。
18. 选举的全过程
两种情况会出现选举:
服务器们启动的时候
服务器运行过程中,Leader 失联了
服务器们启动的选举:
三台服务器 server1、server2、server3:
1.server1 启动,一台机器不会选举。
2. server2 启动,server1 和 server2 的状态改为 looking,广播投票
3. server3 启动,状态改为 looking,加入广播投票。
4. 初识状态,互不认识,大家都认为自己是王者,投票也投自己为 Leader。
5. 投票信息说明,票信息本来为五元组,这里为了逻辑清晰,简化下表达。
初识 zxid = 0,sid 是每个节点的名字,这个 sid 在 zoo.cfg 中配置,不会重复。
节点 | sid |
---|---|
server1 | 1 |
server2 | 2 |
server3 | 3 |
6. 初始 zxid=0,server1 投票(1,0),server2 投票(2,0),server3 投票(3,0)
7. server1 收到 投票(2,0)时,会先验证投票的合法性,然后自己的票进行 pk,pk 的逻辑是先比较 zxid,server1(zxid)=server2(zxid)=0,zxid 相等再比较 sid,server1(sid)< server2(sid),pk 结果为 server2 的投票获胜。server1 更新自己的投票为 (2,0),server1 重新投票。
8. TODO 这里最终是 2 还是 3,需要做实验确定。
9. server2 收到 server1 投票,会先验证投票的合法性,然后 pk,自己的票获胜,server 不用更新自己的票,pk 后,重新在发送一次投票。
10. 统计投票,pk 后会统计投票,如果半数以上的节点投出相同的票,确定选出了 Leader。
11. 选举结束,被选中节点的状态由 LOOKING 变成 LEADING,其他参加选举的节点由 LOOKING 变成 FOLLOWING。如果有 Observer 节点,如果 Observer 不参与选举,所以选举前后它的状态一直是 OBSERVING,没有变化。
简单地说
开始投票 -> 节点状态变成 LOOKING -> 每个节点选自己-> 收到票进行 PK -> sid 大的获胜 -> 更新选票 -> 再次投票 -> 统计选票,选票过半数选举结果 -> 节点状态更新为自己的角色状态。
19. 数据同步全过程
数据同步,麻烦的地方在于有不同的情况。选举结束后的首要工作就是数据同步。Learner 服务器会发送给 Leader 服务器一个数据包,其中的 lastZxid 会表明自己的数据新旧程度。
这里需要三个变量,辅助判断:
peerLastZxid:Follower 最后处理的 Zxid
minCommittedLog:Leader 缓存队列中的最小 Zxid
maxCommittedLog:Leader 缓存队列中的最大 Zxid
1. DIFF 同步
peerLastZxid > minCommittedLog && peerLastZxid < maxCommittedLog
这种情况,就是 Follower 少了一部分数据,直接同步 Leader 即可。
2. TRUNC 同步
peerLastZxid > maxCommittedLog
Follower 的数据比 Leader 多,但是一切要以 Leader 为准,这就需要回滚。多余的数据删除掉。
3. 全量同步(SNAP 同步)
peerLastZxid < minCommittedLog
这时 Follower 已经无法依赖队列的数据进行同步,因为它缺少的不仅仅是增量的数据,这时只能全量同步了。
20. 分布式锁
本地锁,可以用 JDK 实现,但是分布式锁就必须要用到分布式的组件。比如 ZooKeeper、Redis。网上代码一大段,面试一般也不要写,我这说一些关键点。
几个需要注意的地方如下。
死锁问题:锁不能因为意外就变成死锁,所以要用 ZK 的临时节点,客户端连接失效了,锁就自动释放了。
锁等待问题:锁有排队的需求,所以要 ZK 的顺序节点。
锁管理问题:一个使用使用释放了锁,需要通知其他使用者,所以需要用到监听。
监听的羊群效应:比如有 1000 个锁竞争者,锁释放了,1000 个竞争者就得到了通知,然后判断,最终序号最小的那个拿到了锁。其它 999 个竞争者重新注册监听。这就是羊群效应,出点事,就会惊动整个羊群。应该每个竞争者只监听自己前面的那个节点。比如 2 号释放了锁,那么只有 3 号得到了通知。
追问 1:watch 监听为什么是一次性的?
如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
一般是客户端执行 getData(节点 A,true)
,如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。
在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。
追问 2:ZooKeeper 为什么不用数据库做持久化?
1 基础组件,就是基础,不能依赖太多。
追问 3:ZooKeeper 的 session 为什么由 server 维护,client 不行吗?
如果 session 由 client 管理,那么集群宕机了,然后恢复了,session 并不知道 client 失效了,临时节点也不会被清理,对 server 来说它依然是临时节点。
推荐阅读